﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using AnjLab.SyncIT.Core.Api;

namespace AnjLab.SyncIT.Core.Data.GoogleCode
{
    public class GoogleCodeIssueParser
    {
        private int _totalCount = -1;
        private Range _range;
        private readonly Source _source;
        
        public GoogleCodeIssueParser(Source source)
        {
            _source = source;
        }

        public int TotalCount {
            get { return _totalCount; }
        }

        public class Range
        {
            public readonly int From;
            public readonly int To;

            public Range(int from, int to)
            {
                From = from;
                To = to;
            }
        }

        public Range CurrentRange
        {
            get { return _range; }
        }

        public IList<Issue> ParseIssueList(string issueListURL)
        {
            Library.Log.Debug("Parsing issue list: " + issueListURL);

            string issueListHtml = GetHtml(issueListURL);

            if (ParsePaging(issueListHtml) == 0)
            {
                //  Issue list is empty
                return new List<Issue>(0);
            }

            MatchCollection mc = Regex.Matches(issueListHtml, "detail\\?id=\\d+.*>(?<cell>.*)</a>");

            IList<Issue> result = new List<Issue>(mc.Count);

            for (int i = 0; i < mc.Count; i++ )
            {
                var issue = new Issue
                            {
                                //  Order of the HTML columns was 
                                //  cpecified by the issueListURL argument
                                ID = mc[i].Groups["cell"].Value,
                            };

                result.Add(issue);
            }

            Library.Log.Info("Found " + result.Count + " issues");

            return result;
        }

        private static string GetHtml(string url)
        {
            WebRequest request = WebRequest.Create(url);

            using (WebResponse response = request.GetResponse())
            using (Stream stream = response.GetResponseStream())
            using (var reader = new StreamReader(stream))
            {
                return reader.ReadToEnd();
            }
        }

        private int ParsePaging(string issueListHtml)
        {
            MatchCollection mc = Regex.Matches(
                issueListHtml, "(?<from>\\d+)\\s+-\\s+(?<to>\\d+)\\s+of\\s+(?<total>\\d+)");

            if (mc.Count != 2)
            {
                _range = new Range(0, 0);
                _totalCount = 0;

                return 0;
            }

            _range = new Range(int.Parse(mc[0].Groups["from"].Value),
                               int.Parse(mc[0].Groups["to"].Value));

            _totalCount = int.Parse(mc[0].Groups["total"].Value);

            return _range.To - _range.From + 1;
        }

        public IList<Issue> FindFirst()
        {
            return ParseIssueList(
                "http://code.google.com/p/"
                + _source.ProjectBase
                + "/issues/list?q=Owner=" 
                + _source.UserName
                + "&colspec=ID&cells=tiles");
        }

        public IList<Issue> FindNext()
        {
            if (_range == null)
            {
                throw new InvalidOperationException("FindFirst must be invoked before FindNext");
            }

            if (_totalCount == 0)
            {
                return new List<Issue>(0);
            }

            //  TODO Build new URL using the _range member and invoke ParseIssueList
            return new List<Issue>(0);
        }

        public Issue ParseIssueDetails(string issueDetailsURL)
        {
            Library.Log.Debug("Parsing issue details: " + issueDetailsURL);

            var issue = new Issue();

            string issueDetailsHtml = GetHtml(issueDetailsURL);

            issue.ID = Regex.Match(issueDetailsHtml, "detail\\?id=(?<id>\\d+)").Groups["id"].Value;

            issue.Stars = int.Parse(Regex.Match(issueDetailsHtml, ">\\s+(?<stars>\\d+)\\s+[person|people]").Groups["stars"].Value);

            MatchCollection mc = Regex.Matches(issueDetailsHtml, "class=\"author\"");

            Library.Log.Debug("Found " + mc.Count + " comments");

            //  "Reported by" post
            Issue.Comment reportedBy = ExtractComment(issueDetailsHtml, mc[0]);

            issue.Reporter = reportedBy.Author;
            issue.Opened = reportedBy.Timestamp;
            issue.Description = reportedBy.Message;
            issue.Attachments = reportedBy.Attachments;

            issue.Comments = new Issue.Comment[mc.Count-1];

            issue.Modified = "----";

            //  Comments
            for (int i = 1; i < mc.Count; i++)
            {
                issue.Comments[i - 1] = ExtractComment(issueDetailsHtml, mc[i]);
                issue.Modified = issue.Comments[i - 1].Timestamp;
            }

            issue.Owner = Regex.Match(issueDetailsHtml, "Owner:&nbsp;.*\\s+.*href=\"/u.*>(?<owner>.*)</a>").Groups["owner"].Value;
            issue.Owner = issue.Owner == string.Empty ? "----" : issue.Owner;

            issue.Status = Regex.Match(issueDetailsHtml, "Status:&nbsp;.*\\s+.*\\s+(?<status>.*)").Groups["status"].Value;
            string ccHtml = Regex.Match(issueDetailsHtml, "Cc:&nbsp;.*\\s+(?<cc>.*)").Groups["cc"].Value;
            if (ccHtml.Equals(string.Empty))
            {
                issue.CC = "----";
            }
            else
            {
                var builder = new StringBuilder();
                foreach (Match match in Regex.Matches(ccHtml, ">(?<cc>[^<]*)</a>"))
                {
                    builder.Append(match.Groups["cc"].Value);
                    builder.Append(", ");
                }
                issue.CC = builder.ToString(0, builder.Length - 2);
            }
            issue.Summary = Regex.Match(issueDetailsHtml, "class=vt>\\s+.*>(?<summary>.*)<").Groups["summary"].Value;

            IList<string> labels = new List<string>();
            foreach (Match match in Regex.Matches(issueDetailsHtml, "list\\?q=label:(?<label>[^\"]+)"))
            {
                labels.Add(match.Groups["label"].Value);
            }
            issue.Labels = ((List<string>)labels).ToArray();

            if (issue.IsClosed)
            {
                issue.Closed = issue.Comments.Length != 0
                                   ? issue.Comments[issue.Comments.Length - 1].Timestamp
                                   : reportedBy.Timestamp;
            }
            else
            {
                issue.Closed = "----";
            }

            return issue;
        }

        private Issue.Comment ExtractComment(string issueDetailsHtml, Capture match)
        {
            Library.Log.Debug("Parsing comment...");

            int messageEnd = issueDetailsHtml.IndexOf("</pre>", match.Index);
            string commentHtml = issueDetailsHtml.Substring(match.Index, messageEnd - match.Index + "</pre>".Length);

            var comment = new Issue.Comment(
                Regex.Match(commentHtml, "href=\"#c\\d+\">(?<id>\\d+)</a>").Groups["id"].Value,
                Regex.Match(commentHtml, "href=\"/u.*>(?<reporter>.*)</a>").Groups["reporter"].Value,
                Regex.Match(commentHtml, "class=\"date\" title=\"(?<datetime>.*)\">").Groups["datetime"].Value,
                Regex.Match(commentHtml, "<pre>\\s+(?<description>[\\s\\S.]*)</pre>").Groups["description"].Value);

            int nextComment = issueDetailsHtml.IndexOf("class=author", messageEnd);

            string postscriptum;

            if (nextComment != -1)
            {
                //  There are more comments followed
                postscriptum = issueDetailsHtml.Substring(messageEnd, nextComment - messageEnd);
            }
            else
            {
                postscriptum = issueDetailsHtml.Substring(messageEnd);
            }

            MatchCollection mc = Regex.Matches(postscriptum, "<td>(?<size>\\d+.*)\\s+.*\\s+href=\"(?<url>.*name=(?<name>.*))\"");

            Library.Log.Debug(mc.Count + " attachments found");

            var attachments = new Issue.Attachment[mc.Count];

            for (int i = 0; i < mc.Count; i++)
            {
                attachments[i] = new Issue.Attachment
                                     {
                                         Name = mc[i].Groups["name"].Value,
                                         Size = mc[i].Groups["size"].Value,
                                         URL = ("http://code.google.com/p/"
                                               + _source.ProjectBase
                                               + "/issues/" + mc[i].Groups["url"].Value).Replace("&amp;", "&")
                                     };
            }

            comment.Attachments = attachments;

            return comment;
        }

        public Issue ParseIssue(string id)
        {
            return ParseIssueDetails(GetIssueDetailsURL(id, _source));
        }

        public static string GetIssueDetailsURL(string id, Source source)
        {
            return "http://code.google.com/p/"
                   + source.ProjectBase
                   + "/issues/detail?id="
                   + id;
        }

        internal void Ping()
        {
            string html = GetHtml("http://code.google.com/p/" + _source.ProjectBase + "/feeds/issueOptions");

            if (_source.UserName == null || ! html.Contains(_source.UserName))
            {
                throw new InvalidOperationException("Invalid Google Code project URL (http://code.google.com/p/" 
                    + _source.ProjectBase + ") or no user \"" 
                    + _source.UserName + "\" registered on the project");
            }

        }
    }
}